package node.exp;

import grammar.Lexer;
import lexical.Key;
import lexical.Word;
import midcode.MidCode;
import midcode.MidcodeGenerator;
import midcode.OPType;
import node.Node;
import symbol.SymbolTable;

import java.util.ArrayList;

public class MulExp implements Node {
    private final Lexer lexer;
    private final SymbolTable table;
    private final Node father;
    private ArrayList<UnaryExp> unaryExps = new ArrayList<>();
    private ArrayList<Word> calTypes = new ArrayList<>();

    public MulExp(Lexer lexer, SymbolTable table, Node father) {
        this.lexer = lexer;
        this.table = table;
        this.father = father;
        parseMulExp();
    }

    private void parseMulExp() {
        unaryExps.add(new UnaryExp(lexer, table, this));
        while (lexer.isWordType(0, Key.MULT) || lexer.isWordType(0, Key.DIV) || lexer.isWordType(0, Key.MOD)) {
            calTypes.add(lexer.getCurWord()); // * | / | %
            unaryExps.add(new UnaryExp(lexer, table, this));
        }
    }

    public int getValue() {
        int value = unaryExps.get(0).getValue();
        for (int i = 1; i < unaryExps.size(); i++) {
            if (calTypes.get(i-1).key == Key.MULT) {
                value *= unaryExps.get(i).getValue();
            } else if (calTypes.get(i-1).key == Key.DIV) {
                value /= unaryExps.get(i).getValue();
            } else {
                value %= unaryExps.get(i).getValue();
            }
        }
        return value;
    }

    @Override
    public Node getFather() {
        return father;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(unaryExps.get(0).toString());
        for (int i = 1; i < unaryExps.size(); i++) {
            sb.append(calTypes.get(i-1).toString());
            sb.append(unaryExps.get(i).toString()).append("<MulExp>\n");
        }
        return sb.toString();
    }

    @Override
    public String genMidCode() {
        MidcodeGenerator mg = MidcodeGenerator.getInstance();
        String tmp1 = unaryExps.get(0).genMidCode();
        for (int i = 1; i < unaryExps.size(); i++) {
            OPType opType = (calTypes.get(i-1).key == Key.MULT) ? OPType.MULT :
                            (calTypes.get(i-1).key == Key.DIV) ? OPType.DIV : OPType.MOD;
            String tmp2 = unaryExps.get(i).genMidCode();
            String tmp3;
            if (mg.isNumber(tmp1) && mg.isNumber(tmp2)) {
                int value1 = Integer.parseInt(tmp1);
                int value2 = Integer.parseInt(tmp2);
                int value3 = (opType == OPType.MULT) ? value1 * value2 :
                             (opType == OPType.DIV) ? value1 / value2 : value1 % value2;
                tmp3 = Integer.toString(value3);
            } else if (opType == OPType.MULT && mg.isNumber(tmp1) && Math.abs(Integer.parseInt(tmp1)) == 1) {
                if (Integer.parseInt(tmp1) == 1) {
                    tmp3 = tmp2;
                } else {
                    tmp3 = mg.genTmp();
                    mg.addMidcode(new MidCode(OPType.SUB, "0", tmp2, tmp3));
                }
            } else if (mg.isNumber(tmp2) && Math.abs(Integer.parseInt(tmp2)) == 1) {
                if (Integer.parseInt(tmp2) == 1) {
                    tmp3 = (opType == OPType.MOD) ? "0" : tmp1;
                } else {
                    if (opType == OPType.MOD) {
                        tmp3 = "0";
                    } else {
                        tmp3 = mg.genTmp();
                        mg.addMidcode(new MidCode(OPType.SUB, "0", tmp1, tmp3));
                    }
                }
            } else if (opType == OPType.MULT && (mg.isNumber(tmp1) && Integer.parseInt(tmp1) == 0 ||
                    mg.isNumber(tmp2) && Integer.parseInt(tmp2) == 0)) {
                tmp3 = "0";
            } else if ((opType == OPType.MOD || opType == OPType.DIV) && mg.isNumber(tmp1) && Integer.parseInt(tmp1) == 0) {
                tmp3 = "0";
            } else {
                tmp3 = mg.genTmp();
                mg.addMidcode(new MidCode(opType, tmp1, tmp2, tmp3));
            }
            tmp1 = tmp3;
        }
        return tmp1;
    }

    private String optimize() {
        MidcodeGenerator mg = MidcodeGenerator.getInstance();
        ArrayList<String> mults = new ArrayList<>();
        ArrayList<String> divs = new ArrayList<>();
        mults.add("1");
        mults.add(unaryExps.get(0).genMidCode());
        divs.add("1");
        boolean calFlag = false;
        String ret = "";
        for (int i = 0; i < calTypes.size(); i++) {
            String tmp = unaryExps.get(i + 1).genMidCode();
            switch (calTypes.get(i).word) {
                case "*":
                    if (calFlag) {
                        String tmp3 = clearMultDivList(mults, divs);
                        if (!ret.equals("")) {
                            mg.addMidcode(new MidCode(OPType.MULT, ret, tmp3, tmp3));
                        }
                        ret = tmp3;
                    }
                    if (mg.isNumber(tmp)) {
                        mults.add(0, Integer.parseInt(mults.get(0)) * Integer.parseInt(tmp) + "");
                    } else {
                        mults.add(tmp);
                    }
                    break;
                case "/":
                    calFlag = true;
                    if (mg.isNumber(tmp)) {
                        divs.add(0, Integer.parseInt(divs.get(0)) * Integer.parseInt(tmp) + "");
                    } else {
                        divs.add(tmp);
                    }
                    break;
                case "%":
                    String tmp3 = clearMultDivList(mults, divs);
                    if (!ret.equals("") && tmp3.charAt(0) == 'T') {
                        mg.addMidcode(new MidCode(OPType.MULT, ret, tmp3, tmp3));
                    }
                    ret = mg.genTmp();
                    mg.addMidcode(new MidCode(OPType.MOD, tmp3, tmp, ret));
                    break;
            }
        }
        String tmp3 = clearMultDivList(mults, divs);
        if (!ret.equals("")) {
            mg.addMidcode(new MidCode(OPType.MULT, ret, tmp3, tmp3));
        }
        ret = tmp3;
        return ret;
    }

    private String clearMultDivList(ArrayList<String> mults, ArrayList<String> divs) {
        MidcodeGenerator mg = MidcodeGenerator.getInstance();
        int val1 = Integer.parseInt(mults.get(0));
        int val2 = Integer.parseInt(divs.get(0));
        int gcf = getGreatestCommonFactor(val1, val2);
        String tmp1 = Integer.toString(val1 / gcf);
        String tmp2 = Integer.toString(val2 / gcf);
        if (val1 / gcf == 0 || val2 / gcf == 0) {
            return Integer.toString(0);
        }

        String tmp3 = mg.genTmp();
        for (int j = 1; j < mults.size(); j++) {
            if (mg.isNumber(tmp1) && Integer.parseInt(tmp1) == 1) {
                if (mults.size() == 2) {
                    tmp1 = mults.get(j);
                } else {
                    mg.addMidcode(new MidCode(OPType.MULT, mults.get(j), mults.get(++j), tmp3));
                    tmp1 = tmp3;
                }
            } else {
                mg.addMidcode(new MidCode(OPType.MULT, tmp1, mults.get(j), tmp3));
                tmp1 = tmp3;
            }
        }
        tmp3 = mg.genTmp();
        for (int j = 1; j < divs.size(); j++) {
            if (mg.isNumber(tmp2) && Integer.parseInt(tmp2) == 1) {
                if (divs.size() == 2) {
                    tmp2 = divs.get(j);
                } else {
                    mg.addMidcode(new MidCode(OPType.MULT, divs.get(j), mults.get(++j), tmp3));
                    tmp2 = tmp3;
                }
            } else {
                mg.addMidcode(new MidCode(OPType.MULT, tmp2, divs.get(j), tmp3));
                tmp2 = tmp3;
            }
        }
        if (mg.isNumber(tmp1) && Integer.parseInt(tmp1) == 1) {
            return tmp2;
        } else if (mg.isNumber(tmp2) && Integer.parseInt(tmp2) == 1) {
            return tmp1;
        } else {
            mg.addMidcode(new MidCode(OPType.DIV, tmp1, tmp2, tmp3));
            mults = new ArrayList<>();
            mults.add("1");
            divs = new ArrayList<>();
            divs.add("1");
            return tmp3;
        }
    }

    private int getGreatestCommonFactor(int a, int b) {
        while(b != 0){
            int temp = a % b;
            a = b;
            b = temp;
        }
        return a;
    }
}

